package com.icw.decoupling.impl.module;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaUpdate;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.lang3.ArrayUtils;
import org.hibernate.criterion.AliasedProjection;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.PropertyProjection;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.internal.CriteriaImpl.OrderEntry;
import org.hibernate.transform.ResultTransformer;
import org.springframework.beans.BeanUtils;
import org.springframework.orm.hibernate5.HibernateTemplate;

import com.icw.basis.BaseGeneric;
import com.icw.basis.BasePoEntity;
import com.icw.decoupling.defi.module.GenericDao;
import com.icw.utility.BeanCtxUtils;
import com.icw.utility.IcwUtils;
import com.icw.utility.PoEntityHelper;
import com.icw.utility.query.IcwCriteria;
import com.icw.utility.query.IcwCriterion;
import com.icw.utility.query.PageCount;

import lombok.Getter;

@Getter
public abstract class GenericDaoImpl<T extends BasePoEntity> extends BaseGeneric<T> implements GenericDao<T> {

	private EntityManager em() {
		return BeanCtxUtils.getBeanByType(EntityManager.class);
	};

	private HibernateTemplate ht() {
		return BeanCtxUtils.getBeanByType(HibernateTemplate.class);
	};

	@Override
	public T expandEntityById(String id) {
		if (id == null) {
			return null;
		} else {
			return em().find(getEntityClass(), id);
		}
	}

	@Override
	public T saveEntity(T obj) {
		T obj1 = em().merge(obj);
		em().flush();
		em().refresh(obj1);
		BeanUtils.copyProperties(obj1, obj);
		return obj;
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<T> query(IcwCriteria criteria, PageCount page) throws Exception {
		criteria = IcwUtils.ifNull(criteria, () -> new IcwCriteria());
		return queryEntitiesByCriteriaWithPage(IcwCriteria.loadCriteria(criteria, getEntityDetachedCriteria()), page);
	}

	@Override
	public int countByCriteria(IcwCriteria criteria) throws Exception {
		criteria = IcwUtils.ifNull(criteria, () -> new IcwCriteria());
		return countByCriteria(IcwCriteria.loadCriteria(criteria, getEntityDetachedCriteria()));
	}

	@Override
	public int updateByCriteria(Map<String, Object> contains, IcwCriteria criteria) throws Exception {
		criteria = IcwUtils.ifNull(criteria, () -> new IcwCriteria());
		CriteriaUpdate<T> cu = em().getCriteriaBuilder().createCriteriaUpdate(getEntityClass());
		Root<T> root = cu.from(getEntityClass());
		Predicate where = whereCase(criteria, root);
		if (where == null) {
			return 0;
		} else {
			Set<String> usefulProperties = PoEntityHelper.getUsefulProperties(getEntityClass());
			Set<String> containsKeySet = new LinkedHashSet<>(contains.keySet());
			Map<String, Object> containsCopy = new LinkedHashMap<>();
			Pattern pattern = Pattern.compile("\\w+");
			for (String key : containsKeySet) {
				Matcher matcher = pattern.matcher(key);
				if (matcher.find() && usefulProperties.contains(matcher.group())) {
					containsCopy.put(key, contains.get(key));
				}
			}
			for (Entry<String, Object> entry : containsCopy.entrySet()) {
				cu.set(getPath(root, entry.getKey()), entry.getValue());
			}
			cu.where(where);
			int num = em().createQuery(cu).executeUpdate();
			if (num > 0) {
				em().flush();
			}
			return num;
		}
	}

	private Predicate whereCase(IcwCriteria cri, Root<T> root) throws Exception {
		CriteriaBuilder cb = em().getCriteriaBuilder();
		List<Predicate> restrictions = createPridicates(cri.criterions, root);
		if (restrictions.size() == 0) {
			return null;
		} else if (restrictions.size() == 1) {
			return restrictions.get(0);
		} else {
			return cb.and(restrictions.toArray(new Predicate[0]));
		}
	}

	private List<Predicate> createPridicates(IcwCriterion[] crions, Root<T> root) {
		CriteriaBuilder cb = em().getCriteriaBuilder();
		List<Predicate> restrictions = new LinkedList<>();
		crions = IcwUtils.ifNull(crions, () -> ArrayUtils.toArray());
		for (IcwCriterion temp : crions) {
			Path<Object> path = getPath(root, temp.property);
			Predicate p = null;
			if ("eq".equals(temp.op) && temp.getValue() == null) {
				p = cb.isNull(path);
			} else if ("ne".equals(temp.op) && temp.getValue() == null) {
				p = cb.isNotNull(path);
			} else if ("eq".equals(temp.op)) {
				p = cb.equal(path, temp.getValue());
			} else if ("ne".equals(temp.op)) {
				p = cb.notEqual(path, temp.getValue());
			} else if ("like".equals(temp.op)) {
				p = cb.like(path.as(String.class), String.class.cast(temp.getValue()));
			} else if ("in".equals(temp.op)) {
				Object[] values = PoEntityHelper.toObjectArray(temp.getValue());
				if (values == null || values.length == 0) {
					p = null;
					restrictions.clear();
					break;
				} else {
					p = cb.and(getPath(root, temp.property).in(values));
				}
			} else if ("or".equals(temp.op)) {
				Predicate[] ps = createPridicates(temp.criterions, root).toArray(ArrayUtils.toArray());
				if (ps.length > 0) {
					p = cb.or(ps);
				}
			} else if ("and".equals(temp.op)) {
				Predicate[] ps = createPridicates(temp.criterions, root).toArray(ArrayUtils.toArray());
				if (ps.length > 0) {
					p = cb.and(ps);
				}
			}
			if (p != null) {
				restrictions.add(p);
			}
		}
		return restrictions;
	}

	private Path<Object> getPath(Root<T> root, String property) {
		String[] paths = property.split("\\.");
		Path<Object> path = root.get(paths[0]);
		for (int i = 1; i < paths.length; i++) {
			path = path.get(paths[i]);
		}
		return path;
	}

	@Override
	public void delete(T obj) throws Exception {
		obj = em().merge(obj);
		em().refresh(obj);
		em().remove(obj);
	}

	@Override
	public int deleteByCriteria(IcwCriteria criteria) throws Exception {
		criteria = IcwUtils.ifNull(criteria, () -> new IcwCriteria());
		CriteriaBuilder cb = em().getCriteriaBuilder();
		CriteriaDelete<T> cd = cb.createCriteriaDelete(getEntityClass());
		cd.where(whereCase(criteria, cd.from(getEntityClass())));
		return em().createQuery(cd).executeUpdate();
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private List queryEntitiesByCriteriaWithPage(DetachedCriteria criteria, PageCount page) throws Exception {
		// 2. execute query.
		if (page != null) {
			page.setCount(countByCriteria(criteria));
			if (page.getCount() == 0) {
				return new ArrayList(0);
			}
		}
		List list = queryForList(criteria, page);
		List list0 = null;
		try {
			Projection projection = removeProjection(getCriteriaImpl(criteria));
			String[] alias;
			if (null != projection) {
				alias = projection.getAliases();
				if (alias != null && alias.length == 1) {
					list = Arrays.asList(list.stream().map(item -> new Object[] { item }).toArray());
				}
				if (list.size() > 0 && list.get(0) != null && list.get(0).getClass() == Object[].class) {
					list0 = (List) list.stream().map(vals -> {
						String[] closAlias = Arrays.asList(alias).toArray(new String[alias.length]);
						Object[] objs = (Object[]) vals;
						Map<String, Object> map = new LinkedHashMap<String, Object>();
						for (int i = 0; i < closAlias.length; i++) {
//							String __alias = closAlias[i];
							if (closAlias[i].indexOf(IcwCriteria.OP_RATE + "_") == 0) {
//								__alias = StringUtils.join(ArrayUtils.remove(closAlias[i].split("_"), 1), "_");
								if ("0".equals(objs[i + 1] + "")) {
									objs[i] = "0%";
								} else {
									objs[i] = new BigDecimal(objs[i] + "").multiply(new BigDecimal("100")).divide(new BigDecimal(objs[i + 1] + ""), 2, BigDecimal.ROUND_HALF_UP) + "%";
								}
								objs = ArrayUtils.remove(objs, i + 1);
								closAlias = ArrayUtils.remove(closAlias, i + 1);
							}
							if (closAlias[i].indexOf("group_concat") == 0) {
								if (objs[i] == null) {
									objs[i] = new Object[0];
								} else {
									objs[i] = Stream.of(((String) objs[i]).split("_\\$split\\$_"))// e
											.map(s -> s.substring(s.indexOf("$$$") + 3))// e
											.map(s -> "null".equals(s) ? null : s)// e
											.toArray();
								}
							}
//							map.put(__alias, objs[i]);
						}
						map.put("values", objs);
						return map;
					}).collect(Collectors.toList());
				}
			}
		} catch (Exception e) {
			IcwUtils.tryCatch(() -> {
				throw e;
			});
			list0 = null;
		}
		// 返回查询结果
		if (list0 != null) {
			return list0;
		} else {
			return list;
		}
	}

	@SuppressWarnings("rawtypes")
	private List queryForList(DetachedCriteria criteria, PageCount page) {
		if (criteria == null) {
			return new ArrayList(0);
		} else if (page == null) {
			if (removeProjection(getCriteriaImpl(criteria)) == null) {
				criteria.setProjection(null);
				criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
			}
			return ht().findByCriteria(criteria);
		} else {
			return ht().findByCriteria(criteria, page.getStart(), page.getLength());
		}
	}

	private Integer countByCriteria(DetachedCriteria criteria) {
		CriteriaImpl impl = getCriteriaImpl(criteria);
		// 移除影响统计分页的属性并统计
		// 原本的Projection
		Projection projection = removeProjection(impl);
		ResultTransformer resultTransformer = removeResultTransformer(impl);
		List<CriteriaImpl.OrderEntry> orderEntrys = removeOrderEntrys(impl);
		// 新的Projection
		//
		Integer count;
		//
		if (ProjectionList.class.isInstance(projection)) {
			ProjectionList oriPl = (ProjectionList) projection;
			int length = oriPl.getLength();
			for (int i = 0; i < length; i++) {
				Projection p = oriPl.getProjection(i);
				if (AliasedProjection.class.isInstance(p)) {
					try {
						Field f = AliasedProjection.class.getDeclaredField("projection");
						f.setAccessible(true);
						p = (Projection) f.get(p);
					} catch (Exception e) {
						IcwUtils.tryCatch(() -> {
							throw e;
						});
					}
				}
				if (PropertyProjection.class.isInstance(p) && p.isGrouped()) {
					criteria.setProjection(Projections.countDistinct(((PropertyProjection) p).getPropertyName()));
					break;
				}
				if (i == length - 1) {
					criteria.setProjection(Projections.rowCount());
				}
			}
			List<?> __count = ht().findByCriteria(criteria);
			count = ((Number) __count.get(0)).intValue();
		} else
		//
		{
			List<?> __count = ht().findByCriteria((criteria.setProjection(Projections.countDistinct("id"))));
			count = ((Number) __count.get(0)).intValue();
		}
		// 还原属性并查询
		// 将之前的Projection,ResultTransformer和OrderBy条件重新设回去
		if (projection != null) {
			criteria.setProjection(projection);
		} else {
			criteria.setProjection(null);
			criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		}
		// 放回transformer似乎有点问题
		if (resultTransformer != null) {
			criteria.setResultTransformer(resultTransformer);
		}
		setFieldValue(impl, "orderEntries", orderEntrys);
		return count;
	}

	private static CriteriaImpl getCriteriaImpl(DetachedCriteria criteria) {
		try {
			Field implField = DetachedCriteria.class.getDeclaredField("impl");
			implField.setAccessible(true);
			return (CriteriaImpl) implField.get(criteria);
		} catch (Exception e) {
			return null;
		}
	}

	private Projection removeProjection(CriteriaImpl impl) {
		Projection projection = impl.getProjection();
		return projection;
	}

	private ResultTransformer removeResultTransformer(CriteriaImpl impl) {
		ResultTransformer transformer = impl.getResultTransformer();
		return transformer;
	}

	@SuppressWarnings("unchecked")
	private List<CriteriaImpl.OrderEntry> removeOrderEntrys(CriteriaImpl impl) {
		try {
			List<CriteriaImpl.OrderEntry> orderEntries;
			orderEntries = (List<OrderEntry>) getFieldValue(impl, "orderEntries");
			setFieldValue(impl, "orderEntries", new ArrayList<Object>());
			return orderEntries;
		} catch (Exception e) {
			return null;
		}
	}

	private Object getFieldValue(final Object object, final String fieldName) {
		try {
			Field field = getDeclaredField(object, fieldName);
			makeAccessible(field);
			Object result = null;
			result = field.get(object);
			return result;
		} catch (Exception e) {
			return null;
		}
	}

	private void setFieldValue(final Object object, final String fieldName, final Object value) {
		Field field = getDeclaredField(object, fieldName);
		if (field == null) {
			throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
		}
		makeAccessible(field);
		try {
			field.set(object, value);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("never happend exception!", e);
		}
	}

	private Field getDeclaredField(final Object object, final String fieldName) {
		return getDeclaredField(object.getClass(), fieldName);
	}

	private static Field getDeclaredField(final Class<?> clazz, final String fieldName) {
		for (Class<?> superClass = clazz; superClass != Object.class; superClass = superClass.getSuperclass()) {
			try {
				return superClass.getDeclaredField(fieldName);
			} catch (NoSuchFieldException e) {
				// Field不在当前类定义,继续向上转型
			}
		}
		return null;
	}

	private void makeAccessible(final Field field) {
		if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
			field.setAccessible(true);
		}
	}

	private DetachedCriteria getEntityDetachedCriteria() {
		return DetachedCriteria.forClass(getEntityClass());
	}
}
